package cn.mapland.yuanlu.updater;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.Map;
import java.util.StringJoiner;
import java.util.TreeMap;

import com.alibaba.fastjson.JSON;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.ToString;
import lombok.Value;

/**
 * 项目更新器, 通过版本托管服务器更新项目
 * 
 * @author yuanlu
 *
 * @see <a href= "https://git.yuanlu.bid/yuanlu/UpdaterClient-core">GitLu</a>
 * @see <a href=
 *      "https://jitpack.io/#bid.yuanlu.git.yuanlu/UpdaterClient-core">maven</a>
 */
public class Updater {

	@SuppressWarnings("javadoc")
	@Value
	public static final class Assets {
		@NonNull TreeMap<String, String>	data;
		String								filename;
		String								reg;
	}

	/** 项目更新器的配置文件 */
	@SuppressWarnings("javadoc")
	@Getter
	@ToString
	@NoArgsConstructor
	@AllArgsConstructor
	public static final class Conf implements Cloneable, Serializable {
		/***/
		private static final long	serialVersionUID	= 5639490470303296268L;
		private static final Conf	DEFAULT				= new Conf();
		/** 服务器地址 */
		private String				baseUrl				= "https://update.yuanlu.bid";
		/** 项目所属类型 */
		private String				type;
		/** 项目名称 */
		private String				name;
		/** 当前版本 */
		private transient String	nowVersion;
		/** 当前版本ID */
		private transient Integer	nowVersionId;
		/** 寻找资源时额外的配置数据 */
		private Map<String, Object>	extraData;
		/** 寻找资源时使用数据匹配 */
		private boolean				useMatch			= true;
		/** 临时下载存放用的文件夹 */
		private File				tmpDir				= new File("./update");
		/** 自动下载时是否使用预览版 */
		private boolean				usePrerelease;
		/** 是否自动更新(核心无此功能) */
		private boolean				autoUpdate;
		/** 是否通知用户更新(核心无此功能) */
		private boolean				noticeUser			= true;

		/**
		 * 使用默认配置构造
		 * 
		 * @param type 项目所属类型
		 * @param name 项目名称
		 */
		public Conf(String type, String name) {
			this.type	= type;
			this.name	= name;
		}

		/** @return 配置文件是否有效 */
		public boolean check() {
			if (name == null || type == null) return false;
			return true;
		}

		@Override
		public Conf clone() {
			try {
				return (Updater.Conf) super.clone();
			} catch (CloneNotSupportedException e) {
				throw new RuntimeException(e);
			}
		}

		/** @param autoUpdate 是否自动更新 */
		public Conf setAutoUpdate(boolean autoUpdate) {
			this.autoUpdate = autoUpdate;
			return this;
		}

		/** @param baseUrl 基础URL, 指向服务器 */
		public Conf setBaseUrl(String baseUrl) {
			this.baseUrl = baseUrl == null ? DEFAULT.getBaseUrl() : baseUrl;
			return this;
		}

		/** @param baseUrl 基础URL, 指向服务器 */
		public Conf setBaseUrl(String host, short port) {
			return this.setBaseUrl(String.format("http://%s:%s", host, port));
		}

		/** @param baseUrl 基础URL, 指向服务器 */
		public Conf setBaseUrl(URL baseUrl) {
			return this.setBaseUrl(baseUrl.toString());
		}

		/** @param extraData 寻找资源文件时额外的匹配数据 */
		public Conf setExtraData(Map<String, Object> extraData) {
			this.extraData = extraData;
			return this;
		}

		/** @param name 项目名称 */
		public Conf setName(String name) {
			this.name = name;
			return this;
		}

		/** @param nowVersion 当前版本tag */
		public Conf setNowVersion(String nowVersion) {
			this.nowVersion = nowVersion;
			return this;
		}

		/** @param nowVersionId 当前版本id(Git的ID) */
		public Conf setNowVersionId(Integer nowVersionId) {
			this.nowVersionId = nowVersionId;
			return this;
		}

		/** @param tmpDir 更新文件夹(文件将下载到此处) */
		public Conf setTmpDir(File tmpDir) {
			this.tmpDir = tmpDir == null ? DEFAULT.getTmpDir() : tmpDir;
			return this;
		}

		/** @param type 要设置的 type */
		public Conf setType(String type) {
			this.type = type;
			return this;
		}

		/** @param useMatch 是否使用资源文件匹配(不启用将返回所有) */
		public Conf setUseMatch(boolean useMatch) {
			this.useMatch = useMatch;
			return this;
		}

		/** @param usePrerelease 是否优先预览版 */
		public Conf setUsePrerelease(boolean usePrerelease) {
			this.usePrerelease = usePrerelease;
			return this;
		}
	}

	/**
	 * 一个版本信息
	 * 
	 * @author yuanlu
	 *
	 */
	@SuppressWarnings("javadoc")
	@Value
	public static final class SimpleVersion {
		/** 内部ID */
		int		id;
		/** 版本ID, 来自代码托管方 */
		Integer	v_id;
		/** 版本标签, 来自发布者 */
		String	v_tag;
		/** 是否是预览版 */
		boolean	pre;

		/** @return 版本信息 */
		public String info() {
			if (v_tag != null) return v_id == null ? v_tag : String.format("%s(%d)", v_tag, v_id);
			return Integer.toString(v_id);
		}

	}

	/**
	 * 版本信息, 包含预览版和稳定版
	 * 
	 * @author yuanlu
	 *
	 */
	@SuppressWarnings("javadoc")
	@Value
	public static final class VersionInfo {
		/** 稳定版信息 */
		SimpleVersion	normal;

		/** 预览版信息 */
		SimpleVersion	prerelease;

		/** @return 是否有更新 */
		public boolean hasNew() {
			return normal != null || prerelease != null;
		}

		/**
		 * 选择一个版本
		 * 
		 * @param 配置文件
		 * @return 版本信息
		 */
		public Boolean isSelectPrerelease(Conf conf) {
			if (prerelease == null && normal == null) return null;
			return normal == null || (conf.usePrerelease && prerelease != null && prerelease.id > normal.id);
		}

		/**
		 * 选择一个版本
		 * 
		 * @param 配置文件
		 * @return 版本信息
		 */
		public SimpleVersion select(Conf conf) {
			if (prerelease == null && normal == null) return null;
			boolean pre = normal == null || (conf.usePrerelease && prerelease != null && prerelease.id > normal.id);
			return pre ? prerelease : normal;
		}
	}

	/** 配置文件 */
	protected final @Getter Conf conf;

	/**
	 * 通过配置文件构造一个更新器
	 * 
	 * @param conf 配置文件
	 */
	public Updater(@NonNull Conf conf) {
		checkRelocation();
		if (!conf.check()) throw new IllegalArgumentException("bad config");
		this.conf = conf.clone();
	}

	/**
	 * Checks that the class was properly relocated.
	 * 
	 * @author org.bstats
	 */
	private final void checkRelocation() {
		// You can use the property to disable the check in your test environment
		if (System.getProperty("yuanlu-updater.relocatecheck") == null || !System.getProperty("yuanlu-updater.relocatecheck").equals("false")) {
			// Maven's Relocate is clever and changes strings, too. So we have to use this
			// little "trick" ... :D
			final String	defaultPackage	= new String(
					new byte[] { 'c', 'n', '.', 'm', 'a', 'p', 'l', 'a', 'n', 'd', '.', 'y', 'u', 'a', 'n', 'l', 'u', '.', 'u', 'p', 'd', 'a', 't', 'e', 'r' });
			final String	examplePackage	= new String(new byte[] { 'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e' });
			// We want to make sure no one just copy & pastes the example and uses the wrong
			// package names
			if (Updater.class.getPackage().getName().startsWith(defaultPackage) || Updater.class.getPackage().getName().startsWith(examplePackage)) {
				throw new IllegalStateException("Updater class has not been relocated correctly!");
			}
		}
	}

	/**
	 * 下载文件
	 * 
	 * @param _url 文件地址
	 * @param name 保存的文件名
	 * @return 下载的文件
	 * @throws IOException IO错误
	 */
	protected File download(String _url, @NonNull String name) throws IOException {
		if (_url == null) return null;
		URL		url	= new URL(_url);
		File	f	= new File(conf.tmpDir, name);
		Files.createDirectories(f.getParentFile().toPath());
		try (InputStream in = url.openStream()) {
			try (FileOutputStream out = new FileOutputStream(f)) {
				byte[]	buf	= new byte[8192];
				int		len;
				while ((len = in.read(buf)) > 0) out.write(buf, 0, len);
			}
		}
		return f;
	}

	/**
	 * 获取版本的资源文件
	 * 
	 * @param version 版本
	 * @return 资源数据
	 * @throws IOException IOE
	 */
	@SuppressWarnings("deprecation")
	public Assets getAssets(@NonNull SimpleVersion version) throws IOException {
		HashMap<String, String> data = new HashMap<>();
		if (conf.useMatch) {
			String os = System.getProperty("os.name"), java = System.getProperty("java.version"), platform = System.getProperty("os.arch");
			data.put("os", os + os.replace(" ", ""));
			data.put("java", java);
			data.put("platform", String.format("%s;%s;", platform, platform.indexOf("64") > -1 ? "x64" : "", platform.indexOf("86") > -1 ? "x86" : ""));
			if (conf.extraData != null) conf.extraData.forEach((k, v) -> data.put(k, data.containsKey(k) ? (data.get(k) + v) : String.valueOf(v)));
		}
		StringJoiner sj = new StringJoiner("&", "?", "");
		data.forEach((k, v) -> sj.add(k + "=" + URLEncoder.encode(v)));

		URL				url		= new URL(new URL(conf.baseUrl),
				String.format("update/assets/%s/%s/%d%s", conf.type, conf.name, version.getId(), conf.useMatch ? sj : ""));

		URLConnection	conn	= url.openConnection();
		try (InputStreamReader in = new InputStreamReader(conn.getInputStream())) {
			StringBuilder	sb		= new StringBuilder();
			char			buf[]	= new char[8192];
			int				i;
			while ((i = in.read(buf)) > 0) sb.append(buf, 0, i);
			return JSON.parseObject(sb.toString(), Assets.class);
		}
	}

	/**
	 * 获取新版本
	 * 
	 * @return 获取的新版本
	 * @throws IOException IOE
	 */
	@SuppressWarnings("deprecation")
	@NonNull
	public VersionInfo getNewVersion() throws IOException {
		StringJoiner sj = new StringJoiner("&", "?", "");
		if (conf.nowVersionId != null) sj.add("id=" + conf.nowVersionId);
		if (conf.nowVersion != null) sj.add("tag=" + URLEncoder.encode(conf.nowVersion));
		URL				url		= new URL(new URL(conf.baseUrl), String.format("update/version/%s/%s%s", conf.type, conf.name, sj));
		URLConnection	conn	= url.openConnection();
		try (InputStreamReader in = new InputStreamReader(conn.getInputStream())) {
			StringBuilder	sb		= new StringBuilder();
			char			buf[]	= new char[8192];
			int				i;
			while ((i = in.read(buf)) > 0) sb.append(buf, 0, i);
			VersionInfo v = JSON.parseObject(sb.toString(), VersionInfo.class);
			return v == null ? new VersionInfo(null, null) : v;
		}
	}
}